using System;
using System.Collections.Generic;
using System.Xml;
using Microformats.Entities;
using Microformats.Interfaces;
using Microformats.Readers.Entities;
using Microformats.Readers.Interfaces;

namespace Microformats.Readers
{
    internal class MicroformatXmlReader : IMicroformatReader, IMicroformatReaderImplementation
    {
        protected XmlReader reader;
        protected MicroformatReader microformatReader;
        private Dictionary<string, string> availableAttributes;
        private Microformat currentMicroformat;

        internal MicroformatXmlReader(XmlReader stream, MicroformatReader currentReader)
        {
            reader = stream;
            microformatReader = currentReader;
            CreateAttributeSet();
        }

        protected MicroformatXmlReader()
        {
        }

        /// <summary>
        /// Creates the attribute set, these are the attributes that we use to look for data on an element.
        /// </summary>
        public void CreateAttributeSet()
        {
            availableAttributes = new Dictionary<string, string>();
            availableAttributes.Add("data", "");
            availableAttributes.Add("alt", "");
            availableAttributes.Add("title", "");
            availableAttributes.Add("value", "");
            availableAttributes.Add("rel", "");
            availableAttributes.Add("href", "");
            availableAttributes.Add("src", "");
        }

        /// <summary>
        /// Reads a microformat from the source stream
        /// </summary>
        /// <returns></returns>
        public Microformat Read()
        {
            while (reader.Read())
            {
                foreach (MicroformatInformation mi in microformatReader.microformatsToFind)
                {
                    if (reader.MoveToAttribute(mi.RootType) && reader.Value.Contains(mi.Root))
                    {
                        //We have found a type that we are looking for, eg class, rel etc.

                        //This will need to be changed if it is possible to have two microformats on the same element.
                        reader.MoveToElement();
                        return ParseMicroformat(reader, mi);
                    }
                    else
                    {
                        reader.MoveToElement();
                    }
                }
            }

            return null;
        }

        /// <summary>
        /// A Microformat has been found therefore we must reader it in.
        /// </summary>
        /// <returns></returns>
        protected void ReadMicroformat(MicroformatInformation currentMicroformatInformation)
        {
            //We definatly have a microformat, so create an instance.
            currentMicroformat = new Microformat();

            currentMicroformat.Name = currentMicroformatInformation.Name;
            //We are inside a microformat so we really need to advance the reader past the current node
            if (currentMicroformatInformation.Fields.Count > 0)
            {
                reader.Read();
            }
            //Iterate accross the reader, seeing if there are any nested microformats.
            while (reader.Read())
            {
                RecursivlyParseElements(reader, currentMicroformatInformation);
            }
        }

        /// <summary>
        /// Recursivlies parses the elements.
        /// </summary>
        /// <param name="currentReader">The current reader.</param>
        /// <param name="currMfInfo">The current microformat inforamtion.</param>
        private string RecursivlyParseElements(XmlReader currentReader, ICommonInformation currMfInfo)
        {
            string accumulatedTextValue = "";
            List<IField> newFields = new List<IField>();
            if (currentReader.IsStartElement() || currentReader.NodeType == XmlNodeType.Attribute)
            {
                //We are about to start reading the element
                currentReader.MoveToElement();

                //See if the class has attributes that we are looking for.
                string attributeList = "";
                string[] attrStrings = null;
                string elementTag = reader.Name;
                if (currentReader.MoveToAttribute(currMfInfo.RootType))
                {
                    attributeList = currentReader.Value;
                    attrStrings = attributeList.Split(' ');
                    //Split the attribute collect 
                    foreach (string attribute in attrStrings)
                    {
                        ICommonInformation field;
                        if ((field = currMfInfo.FindFieldByName(attribute)) != null)
                        {
                            //Find where this field will sit in the Microformat Structure.
                            currMfInfo = CreateFieldFromAttributes(currMfInfo, elementTag, field, newFields);
                        }
                        else if (currMfInfo.ParentField != null && (field = currMfInfo.ParentField.FindFieldByName(attribute)) != null)
                        {
                            //Use the parent.
                            currMfInfo = CreateFieldFromAttributes(currMfInfo.ParentField, elementTag, field, newFields);
                        }
                    }

                    //Move back to the element
                    currentReader.MoveToElement();
                }

                using (XmlReader r = currentReader.ReadSubtree())
                {
                    //We have already read the element so move to the next.
                    r.Read();
                    
                    accumulatedTextValue += ReadFieldData(accumulatedTextValue, currMfInfo, r);

                    AssignSubFieldsToEmptySharedElements(newFields);

                    if (accumulatedTextValue != String.Empty)
                    {
                        //Loop through all the fields, when we find either the machine value empty or human value replace it with the accumulated text.
                        AssignValueToUnassignedFields(accumulatedTextValue, newFields);
                    }

                    return accumulatedTextValue;
                }
            }

            return "";
        }

        /// <summary>
        /// Assigns the sub fields to empty shared elements.
        /// 
        /// For example, when fn and n (nested name info) are supplied on one element, this method ensures that fn has all the same elements as n.
        /// </summary>
        /// <param name="newFields">The list of fields that are at the current level.</param>
        private static void AssignSubFieldsToEmptySharedElements(List<IField> newFields)
        {
            if (newFields.Count > 1)
            {
                List<IField> emptyFields = new List<IField>();
                IField nonEmptyField = null;

                //Find all empty fields and at one non empty field
                foreach (IField field in newFields)
                {
                    if (field.HumanValue == "" && field.MachineValue == "")
                    {
                        //The fields has not been populated
                        emptyFields.Add(field);
                    }
                    else
                    {
                        nonEmptyField = field;
                    }
                }

                //For every empty fields, assign it the data in the non empty field
                if (nonEmptyField != null)
                {
                    foreach (IField field in emptyFields)
                    {
                        field.Fields.AddRange(nonEmptyField.Fields);
                    }
                }
            }
        }

        /// <summary>
        /// Reads the field data.
        /// </summary>
        /// <param name="accumulatedTextValue">The accumulated text value.</param>
        /// <param name="currMfInfo">The curr mf info.</param>
        /// <param name="r">The reader.</param>
        /// <returns></returns>
        private string ReadFieldData(string accumulatedTextValue, ICommonInformation currMfInfo, XmlReader r)
        {
            if (r.IsEmptyElement == false)
            {
                while (r.Read())
                {
                    switch (r.NodeType)
                    {
                        case XmlNodeType.Text:
                            accumulatedTextValue += r.Value;
                            break;
                        case XmlNodeType.Element:
                            accumulatedTextValue += RecursivlyParseElements(r, currMfInfo);
                            break;
                    }
                }
            }
            else
            {
                //There is an empty element, either reader the alt or title.
                ParseAttributeInformationIgnoringValue();
                IField tmpField = new Field(null);
                AssignValues(r.Name, currMfInfo, tmpField);

                accumulatedTextValue += tmpField.HumanValue;
            }

            return accumulatedTextValue;
        }

        /// <summary>
        /// Creates the field from attributes.
        /// </summary>
        /// <param name="currMfInfo">The curr mf info.</param>
        /// <param name="elementTag">The element tag.</param>
        /// <param name="f">The f.</param>
        /// <param name="newFields">The new fields.</param>
        /// <returns></returns>
        private ICommonInformation CreateFieldFromAttributes(ICommonInformation currMfInfo, string elementTag, ICommonInformation f, List<IField> newFields)
        {
            IField parentField = null;
            parentField = currentMicroformat.FindFieldByName(currMfInfo.Name);
                            
            if (f.Fields.Count > 0)
            {
                currMfInfo = f;
            }

            //This node definatly contains microformat field information.
            Field newFieldInformation = new Field(f);
            newFieldInformation.Name = f.Name;

            newFields.Add(newFieldInformation);

            if (parentField != null)
            {
                parentField.Fields.Add(newFieldInformation);
            }
            else
            {
                CurrentMicroformat.Fields.Add(newFieldInformation);
            }

            ParseAttributeInformationIgnoringValue();
            AssignValues(elementTag, f, newFieldInformation);
            return currMfInfo;
        }

        /// <summary>
        /// Assigns the value to unassigned fields.  This is important to do when there are two elements shared on the same css class.
        /// </summary>
        /// <param name="accumulatedTextValue">The accumulated text value.</param>
        /// <param name="newFields">The new fields.</param>
        private void AssignValueToUnassignedFields(string accumulatedTextValue, List<IField> newFields)
        {
            foreach (Field newField in newFields)
            {
                if (newField.MachineValue == String.Empty)
                {
                    newField.MachineValue = accumulatedTextValue;
                }

                if (newField.HumanValue == String.Empty)
                {
                    newField.HumanValue = accumulatedTextValue;
                }
            }
        }


        /// <summary>
        /// Assigns the values of the xml element to the fields or Microformat.
        /// </summary>
        /// <param name="elementTag">The element tag.</param>
        /// <param name="field">The field.</param>
        /// <param name="newField">The new field.</param>
        private void AssignValues(string elementTag, ICommonInformation field, IField newField)
        {
            //Todo handle URI's
            if (elementTag == "abbr")
            {
                //We are dealing with an Abbr so the human value is in the title
                if (availableAttributes.ContainsKey("title"))
                {
                    newField.MachineValue = availableAttributes["title"];
                    newField.HumanValue = availableAttributes["title"];
                }
                else
                {
                    newField.MachineValue = "";
                    newField.HumanValue = "";
                }
            }
            else if (elementTag == "object")
            {
                if (String.IsNullOrEmpty(availableAttributes["data"]) == false)
                {
                    newField.HumanValue = availableAttributes["data"];
                }
                newField.MachineValue = newField.HumanValue;
            }
            else if (elementTag == "a" || elementTag == "area" || elementTag == "img")
            {
                //If the data type is Uri then we must treat the data as a URI, otherwise
                string href = "";
                if (availableAttributes.ContainsKey("href"))
                    href = availableAttributes["href"];
                else if (availableAttributes.ContainsKey("src"))
                    href = availableAttributes["src"];

                
                Uri u;

                if (microformatReader.BaseAddress != String.Empty)
                {
                    Uri baseAddress = new Uri(microformatReader.BaseAddress);
                    u = new Uri(baseAddress, href);
                }
                else
                {
                    u = new Uri(href);
                }

                if (field.DataType == "System.Uri")
                {
                    //Hard coding for the fact that rel-tag is not conistent with the rest of the URI parsing.
                    //Split off the last part of the URL to get the tag... This only occurs for tags.

                    if (availableAttributes.ContainsKey("rel"))
                    {
                        if (availableAttributes["rel"].Contains("tag"))
                        {
                            string tmpUrlValue = u.LocalPath;

                            newField.MachineValue = u.GetComponents(UriComponents.Scheme | UriComponents.Host | UriComponents.Path, UriFormat.UriEscaped); 
                           
                            if (tmpUrlValue.EndsWith("/"))
                            {
                                //Remove the trailing slash
                                tmpUrlValue.Remove(tmpUrlValue.Length - 1);
                            }

                            //Only return the last part of the path.
                            int pathSeperatorPartIndex = tmpUrlValue.LastIndexOf("/");
                            if (pathSeperatorPartIndex > -1)
                                newField.HumanValue = tmpUrlValue.Substring(pathSeperatorPartIndex + 1, (tmpUrlValue.Length - pathSeperatorPartIndex) - 1);
                        }
                    }
                    else
                    {
                        string currentUriScheme =
                            u.GetComponents(UriComponents.Scheme, UriFormat.UriEscaped);
                        if (currentUriScheme == "mailto")
                        {
                            href = availableAttributes["href"];
                            int queryPartIndex = href.LastIndexOf("?");
                            if (queryPartIndex > 0)
                            {
                                href = href.Remove(queryPartIndex);
                            }
                            string newValue = href.Replace("mailto:", "");
                            //Treate as an email address, strip out mailto
                            newField.MachineValue = newValue;
                            newField.HumanValue = newValue;
                        }
                        else
                        {
                            //Treat as a http address
                            newField.MachineValue = u.GetComponents(UriComponents.Scheme | UriComponents.Host | UriComponents.Path, UriFormat.UriEscaped);
                            
                            newField.HumanValue = u.GetComponents(UriComponents.Scheme | UriComponents.Host | UriComponents.Path, UriFormat.UriEscaped);
                            
                        }
                    }
                }
                else if (field.DataType == "System.String")
                {
                    //We are dealing with a non URI data type so get the value or the alt text
                    if (availableAttributes.ContainsKey("alt"))
                    {
                        newField.MachineValue = availableAttributes["alt"];
                        newField.HumanValue = availableAttributes["alt"];
                    }
                }
            }
        }

        /// <summary>
        /// Parses the attribute information ignoring the value (content).
        /// </summary>
        private bool ParseAttributeInformationIgnoringValue()
        {
            //Create the attributes that we need to parse.
            CreateAttributeSet();
            Dictionary<string, string> newAttributeSet = new Dictionary<string, string>();
            foreach (KeyValuePair<string, string> attributePair in availableAttributes)
            {
                string key = attributePair.Key;
                if (reader.MoveToAttribute(key))
                {
                    //The attribute has been found so me must store it.
                    newAttributeSet.Add(key, reader.Value);
                }
            }

            //Replace the availableAttributes with only populated values.
            availableAttributes = newAttributeSet;

            reader.MoveToElement();

            //If there are no found attributes, then it probably has child nodes.
            return availableAttributes.Count > 0;
        }

        /// <summary>
        /// Parses the microformat once it has been found in the stream.
        /// </summary>
        /// <param name="r">The r.</param>
        /// <param name="mi">The mi.</param>
        internal
            Microformat ParseMicroformat(XmlReader r, MicroformatInformation mi)
        {
            //Moves to the element that contains the microformat.
            r.MoveToElement();
            //Move across the data until we find an element

            //Return a reader for all of the current subtree so that we can parse all the data inside.
            Microformat m;
            using (XmlReader subTreeReader = r.ReadSubtree())
            {
                MicroformatXmlReader xmlReader = new MicroformatXmlReader(subTreeReader, microformatReader);
                xmlReader.ReadMicroformat(mi);
                m = xmlReader.CurrentMicroformat;
                subTreeReader.Close();
            }

            //We have a Microformat that may contain many nodes that are not really allowed on the enity because they have been along side a nested node.
            MakeNonNestedFieldsSecret(m.Fields);

            return m;
        }

        /// <summary>
        /// Makes the non-nested fields secret.
        /// </summary>
        private static void MakeNonNestedFieldsSecret(List<IField> fields)
        {
            foreach (Field field in fields)
            {
                if ((field.FieldConfiguration.Fields.Count == 0 && field.FieldConfiguration.Plurality == "Singular") && field.Fields.Count > 0)
                {
                    //Copy The configuration
                    field.SecretFields.AddRange(field.Fields);

                    field.Fields.Clear();
                }
                else if (field.Fields.Count > 0)
                {
                    //There are nested fields so we must fix them too.
                    MakeNonNestedFieldsSecret(field.Fields);
                }
            }
        }

        public
            void Dispose()
        {
            microformatReader = null;
        }

        #region IMicroformatReaderImplementation Members

        public
            MicroformatReader Container
        {
            get { return microformatReader; }
        }

        /// <summary>
        /// Gets the current microformat.
        /// </summary>
        /// <value>The current microformat.</value>
        public Microformat CurrentMicroformat
        {
            get { return currentMicroformat; }
        }

        #endregion
    }
}